本日對應公開 Repo:gitlab-proxy/
本文描述的是單機版 NGINX 代理;公開 repo 的目前版本保留了完整白名單、TLS 驗證、限流與已知限制。
昨天我們成功在 Docker 環境中啟動 Claude Code,同時也分享了兩種方式來免除新建立的容器需要重新登入的問題。
昨天完成的是一個可以快速啟動、快速回收的容器骨架,還不能直接等同於完整沙盒。我的目標是接下來逐層收斂它拿得到的憑證、網路與主機能力,讓 AI Agent 能自主執行,同時把可能造成的損害控制在可驗證的範圍內。
今天要蓋的這台代理,我後來用真實案例跑過一次全鏈驗證,結果是:它立在那裡,而 skill 從來沒有走過它。
每個零件單獨測都是綠的。代理起得來、設定檔正確、憑證注入成功、上游打得通。只有把容器、代理與一份真實的 MR 整條鏈接起來跑,才發現第一個 API 呼叫根本沒有經過代理:skill 的 API base 一直從 MR 網址推導,直接連向 GitLab。
所以今天這篇要這樣讀:下面講的是這台代理怎麼蓋,而它蓋好之後還缺了什麼,留到最後一節。
還記得我們在 Skill 中有提到我們企業內部有使用 GitLab,所以一些流程會與它深度綁定。
因此 AI Agent 一定會有需求與 GitLab 互動、溝通。
但是我們要如何安全地做到這件事?
在加入代理之前,Skill 會從環境變數取得 GitLab access token,再自行帶著憑證呼叫 API。這等於把 token 直接交給 session 裡的 Agent,因此我想把憑證移到它碰不到、卻仍能代它完成驗證的位置。
而且與 GitLab 互動事實上有兩類路徑:
所以以下跟大家分享我是如何設計的。
當時我腦中浮現的構想是電子期刊。若組織有購入電子資源管理平台,你可以用自己的帳號登入該平台,然後到組織有付費的出版社進行文章的閱讀。
這套系統走的模式就是 Proxy 代理伺服器。
出版社的網頁會比對來源 IP,若有對應的紀錄就放行、讓你可以閱讀。
所以仿造這個模式,我們可以自己設計一套 Proxy,並在期間注入 GitLab Token,如此一來,AI Agent 僅僅需要對這個 Proxy 發起請求,就能夠跟 GitLab 伺服器進行溝通了。
所以問題就變成:我要怎麼實作呢?自己寫一套嗎?
當我真的準備打算自己寫一套時,熊熊想到:
有沒有一個誰能作為門面收流量,然後送到後面的伺服器?
對耶,是 Reverse Proxy!而平時我自己在用的是 NGINX。
那就變成只要確認一個地方就可以了,它能否在轉送給後面伺服器(也就是 GitLab)前貼上一個 Header?
呃,不對!
這不就是 NGINX 基礎功能嗎!
那我的金鑰要怎麼給 NGINX 知道?
直接把金鑰寫在 nginx.conf。確實是個方法,但我會想要上 git 耶!
經過一番搜尋我們發現可以透過 envsubst 指令來達成這個目的。
先建立一個 nginx.conf.template 檔案,並且用 ${變數} 的語法在模板中界定出動態的地方。
# nginx.conf.template(這份可以進 git)
# 只示範憑證注入;完整六條白名單請以公開 repo 為準
location = /api/v4/user {
limit_except GET { deny all; }
proxy_set_header PRIVATE-TOKEN "${GITLAB_TOKEN}";
proxy_pass https://gitlab;
}
上面只展示 token 如何注入,不是完整的安全設定。公開 repo 的版本還會逐條限制 path 與 method,並明確開啟 upstream TLS 憑證驗證;NGINX 的 proxy_ssl_verify 預設是 off,不能因為 proxy_pass 使用 https:// 就以為它已經驗證 GitLab 的伺服器憑證。
然後在 Docker Container 啟動時的 command 這樣設定:
command: >
/bin/sh -c "envsubst '$${GITLAB_TOKEN}'
< /etc/nginx/nginx.conf.template
> /etc/nginx/nginx.conf
&& nginx -g 'daemon off;'"
幾個可以分享的點:
envsubst 後面那個參數不能省。
不帶參數的 envsubst 會把所有長得像變數的東西全部替換掉,而且沒定義的直接換成空字串。nginx 設定檔裡到處都是
$remote_addr、$binary_remote_addr、$proxy_add_x_forwarded_for等。
$${GITLAB_TOKEN} 的雙錢字號是給 docker compose 看的
compose 自己會先對 command 做一次變數插值。
寫一個 $ 的話,compose 會先把它換成 .env 裡的真值,於是 envsubst 收到的參數變成 glpat-xxxx,那不是合法的變數引用,什麼都不會被替換,
渲染出來的設定檔裡還留著字面上的 ${GITLAB_TOKEN},nginx 就真的送出
PRIVATE-TOKEN: ${GITLAB_TOKEN}。
GitLab 這兩種寫法都吃:
PRIVATE-TOKEN: <token>:GitLab 自己的標頭,值就是 token 本身、沒有前綴Authorization: Bearer <token>:HTTP 標準的寫法,Bearer是 scheme 名稱(RFC 6750),屬於Authorization這個標頭的語法,不是 token 的一部分我選前者,因為它是 GitLab 原生的形式,而且不會跟代理本身處理
Authorization的那一行打架。⚠ 但兩者不能混著寫。
PRIVATE-TOKEN: Bearer glpat-xxx會讓 GitLab 把「Bearer glpat-xxx」整串當成 token 去比對,然後回你 401。而且錯誤訊息只會說未授權,不會告訴你是標頭寫錯。
gitlab-proxy 存取。這一篇先從單機版開始,把「session 能使用 GitLab API、卻拿不到 API token」這條路驗通。若後續擴成多人使用,每位使用者各自的憑證邊界會是必要條件,不能共用同一組憑證。
GitLab 從 18.10 開始有 fine-grained PAT(19.2 正式推出),可以把 token 的權限切到端點層級。在那之前的版本,只有 api、read_api 這種大方向的 scope。
不過就算有了那一層,它解決的是「token 能做什麼」,不是「token 在誰手上」:代理要解決的是另外三件事。
由於我們的情境明訂在 Skill 中,審查期間會需要的 API 端點是有限可數的,也確實盤點在 Day 9 中。
所以我們自然可以做到有定義在 nginx.conf 中的放行,其餘沒有定義的端點自然不會通。
同樣地,盤點 Skill 的實際操作後,每條 location 只開必要的方法:讀取端點允許 GET 與 HEAD(NGINX 在允許 GET 時會連帶允許 HEAD),發布與回覆只允許 POST,同時承擔讀取與發布的 discussions 集合則開放 GET、HEAD 與 POST;PUT、PATCH、DELETE 等其他方法一律拒絕。
為避免 AI Agent 在短時間內密集操作,這個單機版代理共用 2r/s 的速率上限,並依各端點的使用方式設定不同的 burst 容忍度。這層限制控制的是瞬間請求頻率,不是整場工作可發出的請求總量。
完整的實作請見範例 Repo 的 gitlab-proxy/ 資料夾。
回到開頭那件事。修法是給 skill 一個 NCR_GITLAB_API_BASE 環境變數:設了它,API base 換成代理、token 可以整個不給,PRIVATE-TOKEN 由代理注入。run script 在接上代理的 network 時會自動設好,容器內從頭到尾沒有 GitLab API token。到這一步,API 那一半的「憑證不進 Session」才真的閉環。
⚠ 只有那一半。開頭列的兩類路徑裡,第二類(clone/fetch/push)不走這顆代理:它只代理 API,沒有 git smart HTTP。那條路的憑證怎麼辦,是下一篇的事。
對應修補紀錄:skill 增加 API base override 見 86ea74a;run wrapper 自動接線見 509df6e。這兩筆 commit 證明修了哪些接縫;真實 MR 的全鏈執行則是本文記錄的實機觀察。
今天做的事情,一句話講完是:不再把 GitLab API token 直接交給 session,改由 NGINX 代為注入。
代理本身沒有什麼了不起,就是一台 NGINX。真正花時間的是想清楚三件事:
nginx.conf 又上不了 git。envsubst 把「設定檔進版控、金鑰進環境變數」這兩件事分開,中間那兩個容易踩的坑(參數不能省、$$ 不能少一個)也記在上面了。路徑、方法與速率三層限制,控制的是 Agent 可以借用哪些能力;更前面的憑證邊界,控制的是它能不能直接讀到 token。若 token 仍留在 session 的環境變數裡,Agent 執行的程式與指令就有能力讀取它。一旦指令輸出、除錯資訊、錯誤處理或惡意提示把 token 印出來,它就可能接著進入模型上下文、log 或對外請求;這類外洩也未必會立刻觸發錯誤或警示。
到這裡,Skill 的 API 呼叫已經改走 proxy,token 由 NGINX 注入,不會再交給 session。不過,存取 Git repository 是另一條傳輸路徑;clone、fetch 與 push 可以走 HTTPS 或 SSH。下一篇先聚焦 SSH:如何讓 session 完成身分驗證,卻拿不到私鑰本身。